feat: Part C - Server auth enforcement (access_level, cookie auth, reCAPTCHA, JWT claims)#991
Merged
pyramation merged 4 commits intomainfrom Apr 17, 2026
Conversation
…S migration, reCAPTCHA C1: access_level enforcement via SET TRANSACTION READ ONLY in graphile middleware C2: Cookie-based session auth as fallback when no Bearer token present C3: CORS migration from api_modules to app_auth_settings.allowed_origins with backward compat C4: reCAPTCHA verification middleware for protected mutations (sign-up, password reset)
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…tion - Replace hardcoded app_settings_auth table name with dynamic discovery via metaschema_modules_public.sessions_module -> metaschema.schema_and_table() - Remove allowed_origins from AuthSettings (CORS migration deferred) - Revert cors.ts to legacy api_modules-only approach
Replace metaschema.schema_and_table() (private schema) with a JOIN through metaschema_modules_public.sessions_module and metaschema_public.schema (both public). Also reduces from 3 queries to 2 by combining the module lookup and schema resolution into a single query.
Adds jwt.claims.access_level and jwt.claims.kind to PostgreSQL session settings so PG functions can read them via current_setting(). This lets the DB layer make decisions based on credential type (api_key vs session) and access level (read_write vs read_only) without additional lookups.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Server-side companion to the DB-layer auth changes in constructive-db PR #832. Adds four capabilities to the GraphQL server middleware:
C1 –
access_levelenforcement: Whenauthenticate()returnsaccess_level = 'read_only'(e.g. read-only API keys), the graphile middleware now setsdefault_transaction_read_only = 'on'in pgSettings, making Postgres reject writes at the transaction level.C2 – Cookie-based session auth: The auth middleware now falls back to parsing a
constructive_sessioncookie from the rawCookieheader when noAuthorization: Bearerheader is present. This avoids addingcookie-parseras a dependency.C3 – reCAPTCHA middleware: New
captcha.tsmiddleware gates protected mutations (signUp,resetPassword, etc.) behind Google reCAPTCHA verification whenenable_captchais true in auth settings. The secret key is read fromRECAPTCHA_SECRET_KEYenv var; the public site key lives inapp_auth_settingsfor the frontend. Fully optional — no-op when either the setting or env var is absent.C4 –
kind+access_levelas JWT claims: The graphile middleware now propagatesjwt.claims.access_levelandjwt.claims.kindinto pgSettings, making them available to all PG functions viacurrent_setting('jwt.claims.access_level')andcurrent_setting('jwt.claims.kind'). This lets the DB layer make decisions based on credential type (api_key vs session) and access level (read_write vs read_only) without additional lookups.Auth settings discovery: Settings are loaded dynamically from the tenant DB by joining
metaschema_modules_public.sessions_modulewithmetaschema_public.schema(both public schemas) to resolve the auth settings table's schema and name. No table names are hardcoded and no private schemas are accessed. Fails gracefully if modules or table don't exist yet (pre-migration). Discovery + settings fetch is 2 queries, cached insvcCacheafter first resolution.Review & Testing Checklist for Human
AUTH_SETTINGS_SQL:schemaNameandtableNameare interpolated directly into the query string (line ~105 ofapi.ts). They come frommetaschema_modules_public.sessions_moduleandmetaschema_public.schema(trusted metaschema data), but verify this assumption holds. Consider whether pg identifier quoting (e.g.pg-format) would be prudent.parseCookieTokencallsdecodeURIComponentwhich can throw on malformed%-sequences. There is no try/catch around this call. Confirm this won't crash the auth middleware for garbage cookie values.jwt.claims.access_levelandjwt.claims.kindare now set for all authenticated requests. Verify that no existing PG functions callcurrent_setting('jwt.claims.access_level', ...)orcurrent_setting('jwt.claims.kind', ...)and make assumptions about these being absent. The values are only set when the token has them (guarded byif), but once set they affect the entire PG session.req.body.operationName, but it runs before graphile. Verify thatexpress.json()or another body parser runs upstream soreq.bodyis populated — otherwise the CAPTCHA gate may silently pass all requests through.access_levelandkindon the token depend on the updatedauthenticate()function from constructive-db PR fix(codegen): handle array types, computed fields, and orderBy enums in input-types-generator #832. Confirm that DB migration is deployed before/alongside this server change, or that the optional field handling is sufficient.Recommended test plan
sessions_module+app_settings_authprovisioned → verify auth settings are loaded via the 2-step public-schema discoverycannot execute ... in a read-only transaction)constructive_sessioncookie → verify auth succeeds without a Bearer headerenable_captcha+ setRECAPTCHA_SECRET_KEY→ verify sign-up is gated, sign-in is notSELECT current_setting('jwt.claims.kind')in a PG function → verify it returns'api_key'Notes
{ errors: [...] }responses, consistent with how graphile reports errors.last_used_at,access_level,credential_kind).Link to Devin session: https://app.devin.ai/sessions/12acfda2a5434d2686c63515cfeb2610
Requested by: @pyramation